package org.daochong.lang;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

@SuppressWarnings("unchecked")
public class Properties implements java.io.Serializable, java.lang.Cloneable {

	private static final long serialVersionUID = 1L;

	private static Map<String, String> code;

	static {
		code = new LinkedHashMap<String, String>();
		code.put("\r\n", "<br>");
		code.put(" ", "<spa>");
	}

	public static String DEFAULT_STRING = "addStrings";

	public static String DEFAULT_ADD_MAP = "addMaps";

	public static String DEFAULT_ADD_ALL_MAP = "addAllMaps";

	public static String DEFAULT_ADD_PROP = "addProperties";

	public static String DEFAULT_ADD_ALL_PROP = "addAllProperties";

	public static String DEFAULT_PROP = "setProps";

	public static String DEFAULT_PROP_POS = "addPropPos";

	public static String DEFAULT_PROP_ADD = "addPropPos";

	public static String DEFAULT_FILE = "addFiles";

	public static String DEFAULT_RESOURCES = "addResources";

	public static String decoding(String data) {
		if (data == null || data.length() == 0)
			return data;
		for (String key : code.keySet()) {
			data = data.replaceAll(code.get(key), key);
		}
		return data;
	}

	public static String encoding(String data) {
		if (data == null || data.length() == 0)
			return data;
		for (String key : code.keySet()) {
			data = data.replaceAll(key, code.get(key));
		}
		return data;
	}

	protected String split = "\r\n";

	protected Map<String, List<String>> multiKeys;

	protected List<EntryString> list;

	protected List<Entry> sources;

	protected Map<File, String> files;

	protected Map<File, Long> fileTimes;

	protected Map<Resource, Long> resources;

	public Properties() {
		init();
	}

	public Properties(byte[] prop) {
		init();
		load(prop);
	}

	public Properties(File file) {
		init();
		load(file, true);
	}

	public Properties(File file, boolean watch) {
		init();
		load(file, watch);
	}

	public Properties(File file, String charset) {
		init();
		load(file, charset, true);
	}

	public Properties(File file, String charset, boolean watch) {
		init();
		load(file, charset, watch);
	}

	public Properties(InputStream in) {
		init();
		load(in);
	}

	public Properties(Resource resource) {
		init();
		load(resource);
	}

	public Properties(String prop) {
		init();
		load(prop);
	}

	public Properties(String data, String split) {
		init();
		load(data);
	}

	public void add(Map<String, String> table) {
		if (table == null || table.size() == 0)
			return;
		this.sources.add(new Entry(DEFAULT_ADD_MAP, table));
		for (String key : table.keySet()) {
			if (!this.multiKeys.containsKey(key)) {
				this.createValues(key).add(table.get(key));
			}
		}

	}

	public void add(Properties p) {
		if (p == null)
			return;
		this.sources.add(new Entry(DEFAULT_ADD_PROP, p));
		for (String key : p.keys()) {
			if (!this.multiKeys.containsKey(key)) {
				this.multiKeys.put(key, p.multiKeys.get(key));
			}
		}
	}

	public void addAll(Map<String, String> table) {
		if (table == null || table.size() == 0)
			return;
		this.sources.add(new Entry(DEFAULT_ADD_ALL_MAP, table));
		for (String key : table.keySet()) {
			this.createValues(key).add(table.get(key));
		}
	}

	public void addAll(Properties p) {
		this.sources.add(new Entry(DEFAULT_ADD_ALL_PROP, p));
		this.multiKeys.putAll(p.multiKeys);
	}

	public void addProperty(String key, String value) {
		this.sources.add(new Entry(DEFAULT_PROP_ADD, new EntryString(key, value)));
		List<String> list = this.multiKeys.get(key);
		if (list == null) {
			list = new ArrayList<String>();
			this.multiKeys.put(key, list);
		}
		list.add(value);
		this.list.add(new EntryString(key, value));
	}

	public void clear() {
		this.multiKeys.clear();
	}

	public Properties clone() {
		Properties p = new Properties();
		p.split = this.split;
		p.multiKeys = new LinkedHashMap<String, List<String>>();
		p.list = new ArrayList<EntryString>();
		p.list.addAll(this.list);
		p.sources = new ArrayList<Entry>();
		for (Entry entry : this.sources) {
			p.sources.add(entry);
		}
		for (String key : this.multiKeys.keySet()) {
			p.multiKeys.put(key, this.multiKeys.get(key));
		}
		if (this.files != null) {
			p.files = new LinkedHashMap<File, String>();
			for (File file : this.files.keySet()) {
				p.files.put(file, this.files.get(file));
			}
			p.fileTimes = new LinkedHashMap<File, Long>();
			for (File file : this.fileTimes.keySet()) {
				p.fileTimes.put(file, this.fileTimes.get(file));
			}
		}
		if (this.resources != null) {
			p.resources = new LinkedHashMap<Resource, Long>();
			for (Resource r : this.resources.keySet()) {
				p.resources.put(r, this.resources.get(r));
			}
		}
		return p;
	}

	public boolean containsKey(String key) {
		return this.multiKeys.containsKey(key);
	}

	private List<String> createValues(String key) {
		List<String> list = this.multiKeys.get(key);
		if (list == null) {
			list = new ArrayList<String>();
		}
		return list;
	}

	private int defaultKeyPos(String key) {
		int re = this.getKeyCount(key) - 1;
		if (re < 0) {
			re = 0;
		}
		return re;
	}

	private void fill(Map<String, String> map, String key, String inKey) {
		List<String> list = this.multiKeys.get(key);
		map.put(inKey, list.get(0));
		for (int i = 1; i < list.size(); i++) {
			map.put(inKey + i, list.get(i));
		}
	}

	public boolean getBoolean(String key) {
		return getBoolean(key, defaultKeyPos(key), false);
	}

	public boolean getBoolean(String key, boolean def) {
		return getBoolean(key, defaultKeyPos(key), def);
	}

	public boolean getBoolean(String key, int pos) {
		return getBoolean(key, pos, false);
	}

	public boolean getBoolean(String key, int pos, boolean def) {
		String tmp = getProperty(key, pos);
		if (tmp == null)
			return def;
		try {
			return Boolean.parseBoolean(tmp);
		} catch (Exception e) {
			return def;
		}
	}

	public Date getDate(String key) {
		return getDate(key, defaultKeyPos(key), "yyyy-MM-dd HH:mm:ss");
	}

	public Date getDate(String key, Date date) {
		Date d = getDate(key, defaultKeyPos(key), "yyyy-MM-dd HH:mm:ss");
		return d == null ? date : d;
	}

	public Date getDate(String key, int pos) {
		return getDate(key, pos, "yyyy-MM-dd HH:mm:ss");
	}

	public Date getDate(String key, int pos, Date date) {
		Date d = getDate(key, pos, "yyyy-MM-dd HH:mm:ss");
		return d == null ? date : d;
	}

	public Date getDate(String key, int pos, String format) {
		String tmp = getProperty(key, pos);
		if (tmp == null)
			return null;
		try {
			SimpleDateFormat sdf = new SimpleDateFormat(format);
			return sdf.parse(tmp);
		} catch (Exception e) {

		}
		return null;
	}

	public Date getDate(String key, int pos, String format, Date date) {
		Date d = getDate(key, pos, format);
		return d == null ? date : d;
	}

	public Date getDate(String key, String format) {
		return getDate(key, defaultKeyPos(key), "yyyy-MM-dd HH:mm:ss");
	}

	public Date getDate(String key, String format, Date date) {
		Date d = getDate(key, defaultKeyPos(key), "yyyy-MM-dd HH:mm:ss");
		return d == null ? date : d;
	}

	public double getDouble(String key) {
		return getDouble(key, defaultKeyPos(key), 0.0);
	}

	public double getDouble(String key, double def) {
		return getDouble(key, defaultKeyPos(key), def);
	}

	public double getDouble(String key, int pos) {
		return getDouble(key, pos, 0.0);
	}

	public double getDouble(String key, int pos, double def) {
		String tmp = getProperty(key, pos);
		if (tmp == null)
			return def;
		try {
			return Double.parseDouble(tmp);
		} catch (Exception e) {

		}
		return def;
	}

	public List<EntryString> getEntrys() {
		List<EntryString> list = new ArrayList<EntryString>();
		list.addAll(this.list);
		return list;
	}

	public int getInt(String key) {
		return getInt(key, defaultKeyPos(key), 0);
	}

	public int getInt(String key, int pos) {
		return getInt(key, pos, 0);
	}

	public int getInt(String key, int pos, int def) {
		String tmp = getProperty(key, pos);
		if (tmp == null)
			return def;
		try {
			return Integer.parseInt(tmp);
		} catch (Exception e) {

		}
		return def;
	}

	public int getKeyCount(String key) {
		referesh();
		if (multiKeys.containsKey(key)) {
			return multiKeys.get(key).size();
		}
		return 0;
	}

	public long getLong(String key) {
		return getLong(key, defaultKeyPos(key), 0l);
	}

	public long getLong(String key, int pos, long def) {
		String tmp = getProperty(key, pos);
		if (tmp == null)
			return def;
		try {
			return Long.parseLong(tmp);
		} catch (Exception e) {

		}
		return def;
	}

	public long getLong(String key, long def) {
		return getLong(key, defaultKeyPos(key), def);
	}

	public long getLongPos(String key, int pos) {
		return getLong(key, pos, 0l);
	}

	public String getProperty(String key) {
		return getProperty(key, defaultKeyPos(key));
	}

	public String getProperty(String key, int pos) {
		referesh();
		List<String> list = this.multiKeys.get(key);
		return list != null && pos >= 0 ? list.get(pos) : null;
	}

	public String getProperty(String key, int pos, String def) {
		String re = getProperty(key, pos);
		if (re == null)
			return def;
		return re;
	}

	public String getProperty(String key, String def) {
		String re = getProperty(key);
		if (re == null)
			return def;
		return re;
	}

	public String getSplit() {
		return split;
	}

	public byte[] getStream(String key) {
		return getStream(key, defaultKeyPos(key));
	}

	public byte[] getStream(String key, int pos) {
		String tmp = getProperty(key, pos);
		if (tmp == null)
			return null;
		try {
			return StringUtils.base64Decode(tmp);
		} catch (Exception e) {
			return null;
		}
	}

	private void init() {
		this.multiKeys = new LinkedHashMap<String, List<String>>();
		this.list = new ArrayList<EntryString>();
		this.sources = new ArrayList<Entry>();
	}

	public Set<String> keys() {
		referesh();
		return this.multiKeys.keySet();
	}

	public void load(byte[] data) {
		load(new String(data));
	}

	public void load(File file) {
		load(file, null, false);
	}

	public void load(File file, boolean watch) {
		load(file, null, watch);
	}

	public void load(File file, String charset, boolean watch) {
		if (watch) {
			if (this.files == null) {
				this.files = new LinkedHashMap<File, String>();
				this.fileTimes = new LinkedHashMap<File, Long>();
			}
			this.files.put(file, charset);
			this.fileTimes.put(file, file.lastModified());
		}
		this.sources.add(new Entry(DEFAULT_FILE, file));
		try {
			String str = null;
			if (charset == null) {
				str = StringUtils.toString(new FileInputStream(file));
			} else {
				str = StringUtils.toString(new FileInputStream(file), charset);
			}
			load(str.split(getSplit()));
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}

	public void load(InputStream in) {
		load(StringUtils.toBytes(in));
	}

	public void load(InputStream in, String charset) {
		load(StringUtils.toString(in, charset));
	}

	public void load(Resource resource) {
		if (this.resources == null) {
			this.resources = new LinkedHashMap<Resource, Long>();
		}
		this.resources.put(resource, resource.lastModified());
		this.sources.add(new Entry(DEFAULT_RESOURCES, resource));
		try {
			load(StringUtils.toString(resource.getInputStream()).split(getSplit()));
		} catch (Throwable e) {

		}
	}

	public void load(String config) {
		if (config == null)
			return;
		this.sources.add(new Entry(DEFAULT_STRING, config));
		load(config.split(split));
	}

	public void load(String[] config) {
		if (config == null || config.length == 0)
			return;
		int idx = 0;
		String lastKey = null;
		int lastPos = 0;
		EntryString lastEs = null;
		for (int i = 0; i < config.length; i++) {
			String s = decoding(StringUtils.utfDecode(config[i].trim()));
			if (s.length() == 0 || s.startsWith("#"))
				continue;
			idx = s.indexOf("=");
			if (idx == -1 || s.startsWith("+=")) {
				if (lastKey != null) {
					String v = multiKeys.get(lastKey).get(lastPos) + "\r\n" + s;
					multiKeys.get(lastKey).set(lastPos, v);
					lastEs.setValue(v);
				}
				continue;
			}
			String key = s.substring(0, idx).trim();
			lastKey = key;
			String val = s.substring(idx + 1).trim();
			lastEs = new EntryString(key, val);
			this.list.add(lastEs);
			List<String> values = multiKeys.get(key);
			if (values == null) {
				values = new ArrayList<String>();
				multiKeys.put(key, values);
			}
			values.add(val);
			lastPos = values.size() - 1;
		}

	}

	public boolean needReferesh() {
		if (this.files != null && this.files.size() > 0) {
			for (File file : this.files.keySet()) {
				if (file.lastModified() > this.fileTimes.get(file)) {
					return true;
				}
			}
		} else if (resources != null) {
			for (Resource resource : this.resources.keySet()) {
				if (resource.lastModified() > this.resources.get(resource)) {
					return true;
				}
			}
		}
		return false;
	}

	public void referesh() {
		if (!this.needReferesh())
			return;
		this.multiKeys = new LinkedHashMap<String, List<String>>();
		this.list = new ArrayList<EntryString>();
		for (Entry entry : this.sources) {
			if (entry.getKey().equals(DEFAULT_STRING)) {
				load(((String) entry.getValue()).split(getSplit()));
			} else if (entry.getKey().equals(DEFAULT_ADD_ALL_MAP)) {
				this.addAll((Map<String, String>) entry.getValue());
			} else if (entry.getKey().equals(DEFAULT_ADD_ALL_PROP)) {
				this.addAll((Properties) entry.getValue());
			} else if (entry.getKey().equals(DEFAULT_ADD_MAP)) {
				this.add((Map<String, String>) entry.getValue());
			} else if (entry.getKey().equals(DEFAULT_ADD_PROP)) {
				this.add((Properties) entry.getValue());
			} else if (entry.getKey().equals(DEFAULT_PROP)) {
				EntryString es = (EntryString) entry.getValue();
				this.setProperty(es.getKey(), es.getValue());
			} else if (entry.getKey().equals(DEFAULT_PROP_ADD)) {
				EntryString es = (EntryString) entry.getValue();
				this.addProperty(es.getKey(), es.getValue());
			} else if (entry.getKey().equals(DEFAULT_PROP_POS)) {
				Map<String, Object> map = (Map<String, Object>) entry.getValue();
				EntryString es = (EntryString) map.get("entry");
				int pos = (Integer) map.get("pos");
				this.setProperty(es.getKey(), es.getValue(), pos);
			} else if (entry.getKey().equals(DEFAULT_RESOURCES)) {
				try {
					load(StringUtils.toString(((Resource) entry.getValue()).getInputStream()).split(getSplit()));
				} catch (Throwable e) {

				}
			} else if (entry.getKey().equals(DEFAULT_FILE)) {
				try {
					load(StringUtils.toString((new FileInputStream((File) entry.getValue()))).split(getSplit()));
				} catch (Throwable e) {

				}
			}
		}
	}

	public void removeKey(String key) {
		multiKeys.remove(key);
	}

	public Map<String, String> search(String key) {
		referesh();
		Map<String, String> re = new LinkedHashMap<String, String>();
		for (String tmp : keys()) {
			if (tmp.startsWith(key)) {
				fill(re, tmp, tmp);
			}
		}
		return re;
	}

	public Properties searchProp(String key) {
		referesh();
		Properties p = new Properties();
		for (EntryString es : this.list) {
			p.addProperty(es.getKey(), es.getValue());
		}
		return p;
	}

	public Map<String, String> searchSubKey(String key) {
		referesh();
		Map<String, String> re = new LinkedHashMap<String, String>();
		for (String tmp : keys()) {
			if (tmp.startsWith(key)) {
				fill(re, tmp, tmp.substring(key.length()));
			}
		}
		return re;
	}

	public Map<String, String> searchSubKeyDeep(String key, int deep) {
		return searchSubKeyDeep(key, deep, ".");
	}

	public Map<String, String> searchSubKeyDeep(String key, int deep, String split) {
		Map<String, String> re = searchSubKey(key);
		List<String> remove = new ArrayList<String>();
		for (String tmp : re.keySet()) {
			String[] ar = tmp.split("\\" + split);
			if (ar.length != deep) {
				remove.add(tmp);
			}
		}
		for (int i = 0; i < remove.size(); i++) {
			re.remove(remove.get(i));
		}
		return re;
	}

	public Properties searchSubKeyDeepProp(String key, int deep) {
		return searchSubKeyDeepProp(key, deep, ".");
	}

	public Properties searchSubKeyDeepProp(String key, int deep, String split) {
		Properties re = searchSubKeyProp(key);
		List<String> remove = new ArrayList<String>();
		for (String tmp : re.keys()) {
			String[] ar = tmp.split("\\" + split);
			if (ar.length != deep) {
				remove.add(tmp);
			}
		}
		for (int i = 0; i < remove.size(); i++) {
			re.removeKey(remove.get(i));
		}
		return re;
	}

	public Properties searchSubKeyProp(String key) {
		referesh();
		Properties re = new Properties();
		for (EntryString es : this.list) {
			re.addProperty(es.getKey(), es.getValue());
		}
		return re;
	}

	public Map<String, String> searchSubKeyRemove(String key) {
		referesh();
		Map<String, String> re = new LinkedHashMap<String, String>();
		List<String> keys = new ArrayList<String>();
		for (String tmp : keys()) {
			if (tmp.startsWith(key)) {
				String k = tmp.substring(key.length());
				fill(re, tmp, k);
				keys.add(tmp);
			}
		}
		for (String k : keys) {
			this.multiKeys.remove(k);
		}
		return re;
	}

	public Properties searchSubKeyRemoveProp(String key) {
		referesh();
		Properties re = new Properties();
		List<String> keys = new ArrayList<String>();
		List<EntryString> esList = new ArrayList<EntryString>();
		for (EntryString es : this.getEntrys()) {
			String tmp = es.getKey();
			if (tmp.startsWith(key)) {
				String k = tmp.substring(key.length());
				re.addProperty(k, es.getValue());
				keys.add(tmp);
				esList.add(es);
			}
		}
		this.list.removeAll(esList);
		for (String k : keys) {
			this.multiKeys.remove(k);
		}
		return re;
	}

	public void setProperty(String key, byte[] value) {
		if (value == null)
			return;
		String r = StringUtils.base64Encode(value);
		setProperty(key, r);
	}

	public void setProperty(String key, byte[] value, int pos) {
		if (value == null)
			return;
		String r = StringUtils.base64Encode(value);
		setProperty(key, r, pos);
	}

	public void setProperty(String key, String value) {
		this.sources.add(new Entry(DEFAULT_PROP, new EntryString(key, value)));
		List<String> list = new ArrayList<String>();
		list.add(value);
		this.multiKeys.put(key, list);
		boolean find = false;
		for (EntryString es : this.list) {
			if (es.getKey().equals(key)) {
				es.setValue(value);
				find = true;
				break;
			}
		}
		if (!find) {
			this.list.add(new EntryString(key, value));
		}
	}

	public void setProperty(String key, String value, int pos) {
		List<String> list = createValues(key);
		if (pos < list.size()) {
			list.set(pos, value);
		} else if (pos == list.size()) {
			list.add(value);
		} else {
			throw new RuntimeException(
					new ArrayIndexOutOfBoundsException("Array(" + list.size() + ") index out of range: " + pos));
		}

		EntryString e = new EntryString();
		e.setKey(key);
		e.setValue(value);
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("entry", e);
		map.put("pos", pos);
		Entry entry = new Entry();
		entry.setKey(DEFAULT_PROP_POS);
		entry.setValue(map);
		this.sources.add(entry);
		boolean find = false;
		int p = 0;
		for (EntryString es : this.list) {
			if (es.getKey().equals(key) && (p++) == pos) {
				es.setValue(value);
				find = true;
				break;
			}
		}
		if (!find) {
			this.list.add(new EntryString(key, value));
		}
	}

	public void setSplit(String split) {
		this.split = split;
	}

	public Map<String, String> toMap() {
		Map<String, String> map = new LinkedHashMap<String, String>();
		for (String key : this.multiKeys.keySet()) {
			fill(map, key, key);
		}
		return map;
	}

	public String toString() {
		StringBuffer sb = new StringBuffer();
		for (EntryString es : this.list) {
			sb.append(es.getKey());
			sb.append(" = ");
			sb.append(encoding(es.getValue()));
			sb.append(split);
		}
		return new String(sb);
	}
}